#! /usr/bin/env Python3
#coding=utf-8
"""
读写CSV文件的通用模块
作者: KydGroups
版本: 1.0.0
完成日期: 2024-02-26
修改记录:
"""

import os #导入操作系统模块
import csv #导入CSV文件读写模块

class KydCsv():
    """
    读写CSV文件的通用模块
    """
    def __init__(self):
        """
        :DESCRIPTION:
            初始化文件
        :PARAMS:
            nothing
        :RETURNS:
            nothing
        """
        self.__strFile:str = "" #读取的CSV文件路径
        self.__strSortKey:str = "" #数据排序主键名
        self.__strEncoding:str = 'utf-8' #数据字符编码
        self.__bHeader:bool = False #是否需要数表头数据
        self.__dictDataSet:dict = {} #数据格式设置
        self.__listFields:list = [] #保存数据键值的列表
        self.__listData:list = [] #保存数据的列表
        self.__bInitialed = False #类是否已经初始化设置过参数
        self.__bDataChanged = False #数据是否已经改变
        self.__ReSetClassData() #重置数据


    def __ReSetClassData(self):
        """
        :DESCRIPTION:
            重置类中保存的数据
        :PARAMS:
            nothing
        :RETURNS:
            nothing
        """
        self.__strFile = "" #读取的CSV文件路径
        self.__strSortKey = "" #数据排序主键名
        self.__strEncoding = 'utf-8' #数据字符编码
        self.__bHeader = False#是否需要数表头数据
        self.__dictDataSet = {} #数据格式设置
        self.__listFields.clear() #保存数据键值的列表
        self.__listData.clear() #保存数据的列表
        self.__bDataChanged = False #数据没有改变


    def __DataTypeConvert(self, listData:list=[], dictSet:dict={}):
        """
        :DESCRIPTION:
            转换字典列表中数值的类型,因为csv文件读取时都是字符串形式,需要进行类型转换
        :PARAMS:
            listData: 字典列表,注意,数据会直接修改列表,处理一半出错时原数据出会改变
            dictSet: 类型设置字典,Key表示如下,没有标注的都按字符串格式处理
                'FloatFields': 浮点数字段列表
                'FloatNum': 浮点数保留几个小数点
                'IntFields': 整数字段列表
        :RETURNS
            True:处理成功 False:处理失败
        """
        if not listData:
            return False
        if not dictSet:
            return False
        try:
            bFloat = False #是否处理浮点数值标志位
            nFloatNum = 4
            bInt = False #是否处理整数值标志位
            if 'FloatFields' in dictSet.keys():#如果设置了浮点数字列表
                bFloat = True #处理浮点数
                if 'FloatNum' in dictSet.keys():#如果设置了浮点数有效小数位数
                    nFloatNum = dictSet['FloatNum']
                    if nFloatNum < 2:#有效数字最少要2位
                        nFloatNum = 4
            if 'IntFields' in dictSet.keys():
                bInt = True
            for item in listData:#循环处理每条记录
                if bInt: #如果需要处理整数值
                    for key in dictSet['IntFields']:
                        item[key] = int(item[key])
                if bFloat: #如果需要处理浮点数值
                    for key in dictSet['FloatFields']:
                        item[key] = round(float(item[key]), nFloatNum)
            return True
        except:
            return False


    def SetCSVParam(self, strFile:str='', listFields:list=[], sortKey:str='', dictSet:dict={}, enconding:str='utf-8', bHeader:bool=False, bForce:bool=False):
        """
        :DESCRIPTION:
            设置CSV类的读取参数,如果没有设置参数,则有一些函数无法使用
        :PARAMS:
            strPath: CSV文件路径
            listFields: CSV数据表头
            sortKey: 排序键值
            strEncoding: 字符编码格式
            bHeader: 是否需要表头的数据 True:需要表头 False:默认,不需要表头
            dictSet: 是否需要进行数值类型转换的设置
            bForce: 是否强制设置参数,如果设置了这个参数,则会清空类中的所有数据
        :RETURNS:
            True:设置成功 False:设置失败
        """
        if self.__bInitialed: #如果当前类已经初始化过了,则无法设置参数
            if not bForce: #如果没有设置强制重置
                return False
            else:
                self.__bInitialed = False
                self.__ReSetClassData() #清除数据
        if not strFile: #开始检查并设置参数,如果没有传入文件路径
            return False
        else: #注意这里不需要检查文件是否存在,可能只需要写数据
            self.__strFile = strFile
        if not listFields: #如果没有传入csv数据表头
            return False
        else:
            self.__listFields = listFields
        if not sortKey: #如果没有传入排序主键,则使用listFields的第一个键作为主键
            self.__strSortKey = self.__listFields[0]
        else:
            self.__strSortKey = sortKey
        self.__strEncoding = enconding #字符编码
        self.__dictDataSet = dictSet #数据格式设置,下面的参数不需要检杳
        self.__bHeader = bHeader #是否需要数据表头
        self.__bInitialed = True #设置初始化标志
        return True


    def __BinarySearch(self, value):
        """
        :DESCRIPTION:
            根据主键,二分法查找类中保存的数据,返回数据位置
        :PARAMS:
            value: sortKey键对应的值
        :RETURNS:
            -1:没有对应的数据, nPos:数据对应的位置
        """
        if not value: #没有传入参数
            return -1
        if not self.__listData: #没有数据
            return -1
        try:
            left = 0 #二分法查找左边界
            right = len(self.__listData) #二分法查找右边界
            mid = int((left+right) / 2) #二分法查找中间位置
            key = self.__strSortKey #主键
            while left < right: #只要还有中间位置
                midValue = self.__listData[mid][key] #中间位置的值
                if value == midValue: #如果在中间位置查找到
                    return mid
                elif midValue > value: #如果中间值大于要查找的值
                    right = mid #调整右边界
                else: #否则调整左边界
                    if left == mid: #如果左边界与中间值相等,int运算问题,防止死循环
                        return -1
                    else:
                        left = mid #调整左界面
                mid = int( (left + right) / 2) #重新计算中间位置
            else: #循环结束没有检查区间,则判断左右边界值
                if left == right: #左右边界值相同
                    if value == self.__listData[right][key]: #如果边界值就是要想找的值
                        return left
                return -1 #边界不相同,则表示没有这个值
        except:
            return -1


    def __ReadCSVFile(self, strFile:str='', listFields:list=[], encoding:str='utf-8'):
        """
        :DESCRIPTION:
            读取CSV文件主要操作函数,并将结果生成字典列表
        :PARAMS:
            strPath: CSV文件路径,参数不需要设置,前置函数已经检查过了
            listFields: CSV数据表头
            encoding: 字符编码格式
        :RETURNS:
            None:处理失败 List:读取成功
        """
        try:
            if not os.path.exists(strFile): #如果文件不存在
                print("ERROR----文件不存在 %s !" % strFile)
                return None
            listRead = list()
            with open(strFile, 'r', encoding=encoding) as csvfile:#读取文件
                reader = csv.DictReader(csvfile, listFields) #读取文件
                for item in reader:#循环处理每一行数据，进行字典化
                    line = dict(item)
                    listRead.append(line)
            if not listRead:#如果没有读取到数据
                return None
            else:
                return listRead
        except:
            return None


    def __WriteCSVFile(self, strFile:str='', listData:list=[], listFields:list=[], encoding:str='utf-8', bHeader:bool=True, bAdd:bool=False):
        """
        :DESCRIPTION:
            将数据写入CSV文件主要操作函数
        :PARAMS:
            strPath: CSV文件路径,参数不需要设置,前置函数已经检查过了
            listData: 字典数据列表
            listFields: CSV数据表头
            encoding: 字符编码格式
            bHeader: 是否需要表头的数据 True:需要表头 False:默认,不需要表头
            bAdd: 是否为添加方式,如果是添加方式且文件存在则不写入表头
        :RETURNS:
            True:写入成功 False:写入失败
        """
        try:
            if os.path.exists(strFile): #如果文件不存在,则判断是否为添加方式
                if bAdd: #如果文件存在且是添加方式
                    with open(strFile, 'a', encoding=encoding) as csvfile:#打开数据文件添加
                        writer = csv.DictWriter(csvfile, listFields)
                        for item in listData:#按顺序写入数据
                            writer.writerow(item)
                    return True

            with open(strFile, 'w', encoding=encoding) as csvfile: #当文件不存在或不是添加方式,打开数据写入
                writer = csv.DictWriter(csvfile, listFields)
                if bHeader:#如果需要写入表头
                    writer.writeheader()#写入数据表头
                for item in listData:#按顺序写入数据
                    writer.writerow(item)
            return True
        except ValueError as Ve:
            print("Error writing to file '%s':%s"% (strFile, Ve))
            return False


    def DirectReadFile(self, strFile:str='', listFields:list=[], sortKey:str='', bReverse:bool=False, encoding:str='utf-8', bHeader=False, dictSet:dict={}):
        """
        :DESCRIPTION:
            直接读取CSV文件内容,并将结果生成字典列表,不保存在类中
        :PARAMS:
            strPath: CSV文件路径
            listFields: CSV数据表头
            sortKey: 排序键值
            Reverse: 如果需要排序，是否反转
            encoding: 字符编码格式
            bHeader: 是否需要表头的数据 True:需要表头 False:默认,不需要表头
            dictSet: 是否需要进行数值类型转换的设置
        :RETURNS:
            None:处理失败 List:读取成功
        """
        if not strFile:#如果没有输入文件路径
            return None
        if not listFields:
            return None
        try:
            listReturn = self.__ReadCSVFile(strFile=strFile, listFields=listFields, encoding=encoding) #读取数据
            if not listReturn:#如果没有读取到数据
                return None
            else:#如果读取到数据
                if not bHeader:#如果不需要表头的数据
                    listReturn.pop(0) #弹出第一行数据
                if sortKey:#如果需要排序
                    listReturn.sort(key=lambda x:x[sortKey], reverse=bReverse)
                if not dictSet:#如果没有设置数值类型转换
                    pass
                else:
                    self.__DataTypeConvert(listData=listReturn, dictSet=dictSet) #进行数值转换
                return listReturn #注意数据没有隔离,在外面修改了数据也会改变类数据
        except:
            return None


    def DirectWriteFile(self, strFile:str='', listData:list=[], listFields:list=[], sortKey:str='', bReverse=False, encoding:str='utf-8', bHeader:bool=True):
        """
        :DESCRIPTION:
            将字典列表中的数据直接写入CSV文件中
        :PARAMS:
            strFile: CSV文件路径
            listData: 字典数据列表
            listFields: CSV数据表头
            sortKey: 排序键值
            bReverse: 如果需要排序，是否反转
            encoding: 字符编码格式
            bHeader: 是否需要表头的数据 True:需要表头 False:默认,不需要表头
        :RETURNS:
            True:写入成功 False:写入失败
        """
        if not strFile:#如果没有设置文件名
            return False
        if not listData:#如果没有设置数据列表
            return False
        if not listFields:#如果没有设置数据表头
            return False
        if sortKey: #如果需要排序写入
            listData.sort( key=lambda x:x[sortKey], reverse=bReverse)
        return self.__WriteCSVFile(strFile=strFile, listData=listData, listFields=listFields, encoding=encoding, bHeader=bHeader)


    def DirectAddFileData(self, strFile:str='', listData:list=[], listFields:list=[], sortKey:str='', bReverse:bool=False, encoding:str='utf-8', bHeader:bool=False):
        """
        :DESCRIPTION:
            将字典列表中的数据添加到CSV文件尾部
        :PARAMS:
            strFile: CSV文件路径
            listData: 字典数据列表
            listFields: CSV数据表头
            sortKey: 排序键值
            bReverse: 如果需要排序，是否反转
            encoding: 字符编码格式
            bHeader: 是否需要表头的数据 True:需要表头 False:默认,不需要表头
        :RETURNS:
            True:写入成功 False:写入失败
        """
        if not strFile:#如果没有设置文件名
            return False
        if not listData:#如果没有设置数据列表
            return False
        if not listFields:#如果没有设置数据表头
            return False
        if sortKey:
            listData.sort( key=lambda x:x[sortKey], reverse=bReverse)
        return self.__WriteCSVFile(strFile=strFile, listData=listData, listFields=listFields, encoding=encoding, bHeader=True, bAdd=True) #执行添加写入


    def ReadFile(self, bForce:bool=False):
        """
        :DESCRIPTION:
            读取CSV文件内容,读取的数据保存在类中,使用参数已经设置过的,如果没有设置参数,则失败
        :PARAMS:
            bForce: 如果类中已经有数据,则清空数据强制读取文件
        :RETURNS:
            True:读取成功 False:读取失败
        """
        if not self.__bInitialed: #如果没有初始化
            return False
        if len(self.__listData) >= 1: #如果类中已经有数据
            if not bForce:
                return False
            else:
                self.__listData.clear() #清空数据
        try:
            listReturn = self.__ReadCSVFile(strFile=self.__strFile, listFields=self.__listFields, encoding=self.__strEncoding) #读取数据
            if not listReturn: #如果没有读取到数据
                return False
            else: #如果读取成功
                if not self.__bHeader: #如果不需要头部
                    listReturn.pop(0) #弹出第一行数据
                listReturn.sort(key=lambda x:x[self.__strSortKey]) #对数据进行排序
                if not self.__dictDataSet: #如果没有设置数据类型转换
                    pass
                else:
                    self.__DataTypeConvert(listData=listReturn, dictSet=self.__dictDataSet)
                self.__listData = listReturn
                return True
        except:
            return False


    def WriteFile(self, otherFile:str=''):
        """
        :DESCRIPTION:
            将类中保存的数据写入文件
        :PARAMS:
            otherFile: 如果设置了这个文件,则将数据另写入文件;否则写入源文件
        :RETURNS:
            True:写入成功 False:写入失败
        """
        if len(self.__listData) <= 0: #如果类中没有保存数据
            return False
        if not self.__bDataChanged: #如果数据没有改变
            return False
        try:
            strFile = '' #要写入的文件名
            if not otherFile: #如果传入另存文件名,则使用保存的文件名
                strFile = self.__strFile
            else: #使用另存文件名
                strFile = otherFile
            if self.__WriteCSVFile(strFile=strFile, listData=self.__listData, listFields=self.__listFields, encoding=self.__strEncoding): #如果写入文件成功
                self.__bDataChanged = False #重置数据改变操作
                return True
            else:
                return False
        except:
            return False


    def GetData(self, nBegin:int=0, nSize:int=0):
        """
        :DESCRIPTION:
            获取类中保存的数据,注意,获取的数据改变不会改变类中的数据
        :PARAMS:
            nBegin: 从第几个元素开始获取,序号从0开始
            nSize: 获取多长的数据,0表示获取全部
        :RETURNS:
            None:参数错误 List:有数据返回
        """
        nLen = len(self.__listData) #获取类中保存的数据长度
        if nLen <= 0: #如果没有数据
            return None
        if not (0 <= nBegin < nLen): #如果开始位置溢出
            print("ERROR----列表位置溢出!")
            return None
        try:
            listSplit = [] #保存切片数据
            num = 0
            nPos = nBegin #读取的数据开始位置
            if nSize <= 0: #如果需要全部数据
                nSize = nLen
            while num < nSize: #循环获取需要的数据
                num += 1 #增加一个计数
                if nPos >= nLen: #如果列表位置溢出
                    break
                item = self.__listData[nPos]
                dictNew = dict() #字典数据浅拷贝
                for key in self.__listFields:
                    dictNew[key] = item[key]
                listSplit.append(dictNew)
                nPos += 1 #增加位置下标
            return listSplit
        except:
            return None


    def SearchData(self, value, strKey:str='', nLimit=1, bLike:bool=False):
        """
        :DESCRIPTION:
            查找数据
        :PARAMS:
            value: 要查找的值
            strKey: 对应的键值,如果没有输入,则使用类中保存的主键
            nLimit: 如果有多条数据,则限制多少条,如果为0,则取最大数
            bLike: 是否近似查找,如果设置了该项,则键值只能为str类型,否则会出错
        :RETURNS:
            None: 出错,或没有数据 List:查找到数据,只有一条也是list类型返回
        """
        if not value: #如果没有传入查找值
            return None
        if not self.__bInitialed: #如果没有初始化
            return None
        if not self.__listData: #如果没有数据
            return None
        try:
            listReturn = []
            nMax =  len(self.__listData) if (nLimit <= 0)  else nLimit #获取查找条数
            nSize = 0
            if not strKey: #如果没有传入键值,则使用主键
                strKey = self.__strSortKey
            for item in self.__listData: #从头开始判断
                bAdd = False #添加标志
                if bLike: #如果在近似查找
                    if item[strKey].find(value) != -1: #如果近似查找到
                        bAdd = True #标记添加
                else:
                    if item[strKey] == value: #如果精确查找相等
                        bAdd = True #标记添加
                if bAdd: #如果要添加
                    dictAdd = dict()
                    for key in self.__listFields: #浅拷贝数据
                        dictAdd[key] = item[key]
                    listReturn.append(dictAdd)
                    nSize += 1 #增加记数
                if nSize >= nMax: #达到限制条数
                    break
            return listReturn
        except: #错误
            return None


    def AddData(self, dictAdd:dict={}):
        """
        :DESCRIPTION:
            添加数据
        :PARAMS:
            dictAdd: 要添加的数据,注意该字典键必须包含初始化时的全部键,且主键不能重复
        :RETURNS:
            False: 出错,或有重复数据 True:添加成功
        """
        if not dictAdd: #如果没有传入参数
            return False
        if not self.__bInitialed: #如果没有初始化
            print("CSV类没有初始化")
            return False
        try:
            for key in self.__listFields: #检查传入字典的key值是否包含设置的fields
                if key not in dictAdd.keys():#如果出现没有包含的key
                    print("参数中Key值不全")
                    return False
            nPos = self.__BinarySearch(dictAdd[self.__strSortKey]) #根据主键想找有没有重复的数据
            if nPos != -1: #如果有重复的值
                print("CSV---->>Add有重复的主键数据!", dictAdd[self.__strSortKey])
                return False
            dictNew = dict()
            for key in self.__listFields:
                dictNew[key] = dictAdd[key]
            self.__listData.append(dictNew)
            self.__listData.sort(key=lambda x:x[self.__strSortKey]) #对数据按主键进行排序
            self.__bDataChanged = True #设置数据改变标志
            return True
        except: #出错
            return False


    def AddMultiData(self, listAdd:list=[]):
        """
        :DESCRIPTION:
            批量添加数据
        :PARAMS:
            listAdd: 要添加的数据列表,只有一个数据要使用list格式,并且不会检查数据重复性
        :RETURNS:
            False: 出错,或有重复数据 True:添加成功
        """
        if not listAdd: #如果没有传入参数
            return False
        if not self.__bInitialed: #如果没有初始化
            print("CSV类没有初始化")
            return False
        try:
            for key in self.__listFields: #检查传入字典的key值是否包含设置的fields,只检查第一个数据
                if key not in listAdd[0].keys():#如果出现没有包含的key
                    print("参数中Key值不全")
                    return False
            listTemp = [] #添加用数据
            for item in listAdd: #循环添加
                nPos = self.__BinarySearch(item[self.__strSortKey]) #根据主键想找有没有重复的数据
                if nPos != -1: #如果有重复的值
                    print("CSV---->>Add有重复的主键数据!", item[self.__strSortKey])
                else:
                    dictNew = dict()
                    for key in self.__listFields:
                        dictNew[key] = item[key]
                    listTemp.append(dictNew) #将数据添加进暂时列表
            if len(listTemp) >= 1:#如果有数据需要添加
                for item in listTemp: #将临时列表中数据添加进类数据列表中
                    self.__listData.append(item)
                self.__listData.sort(key=lambda x:x[self.__strSortKey]) #对数据按主键进行排序
                self.__bDataChanged = True #设置数据改变标志
                return True
            else: #没有数据需要添加
                return False
        except: #出错
            return False


    def DelData(self, dictDel:dict={}):
        """
        :DESCRIPTION:
            删除数据,注意,会删除全部主键相同的数据
        :PARAMS:
            dictDel: 要删除的数据,只要主键包含在设置的键中就可以
        :RETURNS:
            False:出错 True:删除成功
        """
        if not dictDel: #如果没有传入参数
            return False
        if not self.__bInitialed: #如果没有初始化
            print("CSV类没有初始化")
            return False
        if not self.__listData: #如果没有数据
            return False
        try:
            if self.__strSortKey not in dictDel.keys():#如果主键没有包含在参数中
                print("CSV---->>Del 参数没有主键: %s" % self.__strSortKey)
                return False
            bDel = False
            while True:
                nPos = self.__BinarySearch(dictDel[self.__strSortKey]) #根据主键查找有没有重复的数据
                if nPos == -1: #如果没有查找到对应的数据
                    if not bDel: #如果一个也没有删除,表示一个数据也没有查找到
                        print("CSV---->>Del 没有查找到数据!", dictDel[self.__strSortKey])
                    break #中断循环
                else: #如果查找到对应的数据,则删除
                    self.__listData.pop(nPos) #根据位置删除数据
                    bDel = True
            if bDel:
                self.__bDataChanged = True #设置数据改变标志
                return True
            else:
                return False
        except: #出错
            return False


    def UpdateData(self, dictUpdate:dict={}):
        """
        :DESCRIPTION:
            更新数据,注意只会更新查找到的第一个数据
        :PARAMS:
            dictUpdate: 要更新的数据,注意该字典键必须包含初始化时的全部键
        :RETURNS:
            False:出错 True:更新成功
        """
        if not dictUpdate:
            return False
        if not self.__bInitialed: #如果没有初始化
            print("CSV类没有初始化")
            return False
        if not self.__listData: #如果没有数据
            return False
        try:
            for key in self.__listFields: #检查传入字典的key值是否包含设置的fields
                if key not in dictUpdate.keys():#如果出现没有包含的key
                    print("参数中Key值不全")
                    return False
            nPos = self.__BinarySearch(dictUpdate[self.__strSortKey]) #根据主键查找有没有对应的数据
            if nPos == -1: #如果有对应的值
                print("CSV---->>Update 没有查找应对的数据!", dictUpdate[self.__strSortKey])
                return False
            else: #查找到数据位置
                for key in self.__listFields: #更全部非主键的值
                    if key != self.__strSortKey: #注意主键值不更新
                        self.__listData[nPos][key] = dictUpdate[key]
                self.__bDataChanged = True #设置数据改变标志
                return True
        except: #出错
            return False
